iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 17
1
自我挑戰組

從零開始的Flutter世界系列 第 17

Day17 Flutter 的狀態管理 BLoC (一)

  • 分享至 

  • xImage
  •  

Flutter 在一開始其實就提供了一種狀態管理方式,StatefulWidget,然而它僅適合用於在單個Widget 內部維護其狀態,但是專案開發時,可能需要不同的頁面共享變數的狀態,且當這個狀態發生改變時,所有依賴這個狀態的 UI 都需要隨之發生改變,此時StatefulWidget就不太適合,雖然可以透過設置callback在多個widget 之間傳遞狀態,但是當專案複雜度一深,很容易大大增加我們程式碼的耦合度

可參考邦友寫的 高內聚與低耦合

為了解決此問題,官方提供了一些解決方案,網上有各種方式的介紹以及比較,這邊就先以官方推薦的為主來介紹,而之前官方推薦的是 Bloc ,但去年改推薦Provider,我們之後將針對這兩種做介紹

首先,因為Bloc 將會大量使用到Stream ,我們需要先為各位介紹Stream

Stream

官方文件

流 ( Stream ),是一系列非同步的資料佇列 ( 先進先出 (FIFO) ),例如說:介面上使用者觸發的動作 (像是點餐) 等等,可以使用await for ( 非同步的for 迴圈 ) 或 listen()方法來監聽Stream,來處理要接收到的資料

StreamFuture都是Dart中非同步程式設計的核心內容,Future為一次性取得非同步的資料,Stream為取得多次非同步的資料

建立 Stream

以下我們介紹幾種常見的建立 Stream 方式

  1. 轉換 Steam

    假如已經有了一個Stream,但是它的值不是我們想要的,Stream提供了map()where()expand(),以及take()方法,能夠輕鬆將已有的Stream 事件轉化為新的事件

    例如:把整數事件流轉成字串

    import 'dart:async';
    
    Future<String> _intStreamToStringStream(Stream<int> stream) async {
      return await stream.map((event) => event.toString()).join(',');
    }
    
    Stream<int> testStream() async* {
      for (int i = 1; i <= 5; i++) {
        yield i;
      }
    }
    
    main() async {
      var test = testStream();
      dynamic result = await _intStreamToStringStream(test);
      print('intStreamToStringStream result:$result'); 
      //印出 intStreamToStringStream result:1,2,3,4,5
    }
    
  2. 使用StreamController創建

    • StreamController有一個入口:sink,能使用add()將資料加入裡
    • StreamController有一個出口:Stream,資料從sink 加入後,經過StreamController 處理,處理完會把結果從stream傳出來
    • 為了能接收stream傳出來的結果,我們需要listen()方法來一直監聽此stream
    import 'dart:async';
    
    main() {
      StreamController anyController = StreamController(); //未定義類型,任意型態皆可的Stream
      anyController.sink.add(123);
      anyController.sink.add("abc");
      anyController.sink.add(3.14);
    
      StreamController<int> intController = StreamController(); //指定整數型態的Stream
      intController.sink.add(123);
    
      //透過 listen(),監聽一個Stream,當有事件發出時,即會觸發listener
      anyController.stream.listen((data) => print("anyController:$data"));
      intController.stream.listen((data) => print("intController:$data"));
    }
    /*印出
    anyController:123
    intController:123
    anyController:abc
    anyController:3.14
    */
    
  3. 使用async*建立Stream

    如果我們有一系列事件需要處理,這時候可以使用async* 以及 yield來生成一個Stream

    範例:

    商品皆為10元 的商店

    import 'dart:async';
    
    Future<int> getTotalPrice(Stream<int> stream) async {
      var totalPrice = 0;
    
      //透過 await for 使用由建構傳進來的Stream,能夠在此整數事件流中的每個事件到來的時候處理它,當迴圈結束時,函數將暫停,直到下一個事件到達或流完成為止,需搭配 async 使用
      await for (var numbers in stream) { 
        print("數量:$numbers");
        totalPrice = numbers * 10; //收到我們整數事件流的事件
      }
      return totalPrice;
    }
    
    /*
    商品數量的Stream
    async*:創建Stream 的一種方法
    yield:在Stream 上發出事件
    */
    Stream<int> countStream(int to) async* {
      for (int i = 1; i <= to; i++) {
        yield i;
      }
    }
    
    main() async {
      var count = countStream(5);
      var totalPrice = await getTotalPrice(count);
      print('總金額: $totalPrice');
    }
    
    /* 印出
    數量:1
    數量:2
    數量:3
    數量:4
    數量:5
    總金額: 50
    */
    

錯誤事件處理

在某些情況下,流完成之前會發生錯誤;可能是網路從伺服器上取得文件時發生異常,或者創建事件的代碼存在錯誤等等,而流還可以傳遞錯誤事件,就像傳遞一般事件一樣 ( 大多數流將在出現第一個錯誤後就會停止 )

import 'dart:async';

Future<int> getTotalPrice(Stream<int> stream) async {
  var totalPrice = 0;

  try {
    await for (var numbers in stream) {
      print("數量:$numbers");
      totalPrice = numbers * 10;
    }
  } catch (e) {
    return -1;
  }
  return totalPrice;
}

Stream<int> countStream(int to) async* {
  for (int i = 1; i <= to; i++) {
    if (i == 7) {
      throw new Exception(i);
    } else {
      yield i;
    }
  }
}

main() async {
  var count = countStream(10);
  var totalPrice = await getTotalPrice(count);
  print('總金額: $totalPrice');
}
/*印出
數量:1
數量:2
數量:3
數量:4
數量:5
數量:6
總金額: -1
*/

Stream 的種類

Stream 有兩種

  • Single-subscription Streams
  • Broadcast Streams
Single-subscription Streams

單一訂閱流最常見的流包含一系列事件,事件必須以正確的順序傳遞,並且不能丟失任何事件。像是在讀取文件或接收Web請求時獲得的流,它在被監聽之前不會生成事件,並且在取消監聽後它會停止發送事件,這樣的流只能被訂閱監聽一次,即使在第一個訂閱被取消後,也不允許在單個訂閱流上進行兩次監聽

import 'dart:async';

main() {
  StreamController controller = StreamController();

  controller.stream.listen((data) => print(data));
  controller.stream.listen((data) => print(data));

  controller.sink.add("test");
}
//執行後出現異常:Exception: Bad state: Stream has already been listened to.
Broadcast Streams

廣播流允許任意數量的監聽,且不管是否被監聽,它都會生成事件,所以之後才監聽的無法收到之前的事件,不過可以隨時開始監聽這樣的流

如果在Single-subscription Streams想要進行多次監聽,可以使用asBroadcastStream在非廣播流上建立廣播流

import 'dart:async';

main() {
  StreamController controller = StreamController();

  Stream stream = controller.stream.asBroadcastStream();

  stream.listen((data) => print(data));
  stream.listen((data) => print(data));

  controller.sink.add("test");
}
/*印出
test
test
*/

簡單介紹完Stream,我們來介紹一下Bloc 吧

BLoC

版本:bloc 6.0.3

參考文件

全名為Business Logic Component,使我們能把商業邏輯從UI 抽離,降低程式碼彼此之間的耦合性,讓Widget 只需要著重於UI,顯示處理後的結果畫面,這讓我們達成幾項目的:

  1. 因為商業邏輯與UI 相分離,所以當商業邏輯或畫面需要修改時,能讓彼此之間因為修改造成的影響降至最低
  2. 方便我們測試商業邏輯功能
  3. 提高我們程式碼的可重用性

使用

  1. blocflutter_bloc的包作為依賴項(dependencies) 添加到我们的pubspec.yaml

    dependencies:
      bloc: ^6.0.3
      flutter_bloc: ^6.0.5
    
  2. 在要用到bloc 的地方再透過import 引入即可

Cubit

Cubit 是Bloc 運作的基礎 ( Bloc 繼承了Cubit ),Cubit 是一種特殊類型的Stream,可以用來管理任何類型的狀態。

Stream:

https://ithelp.ithome.com.tw/upload/images/20201002/20118479A351r2b0yi.png

Cubit需要一個初始狀態,是在發出調用之前的狀態,可以通過狀態getter取得cubit 的當前狀態,並可以通過調用帶有新狀態的emit方法來更新cubit 的狀態

https://ithelp.ithome.com.tw/upload/images/20201002/20118479tpaBRtFNkp.png

當我們調用事先定義的函數,cubit 狀態就會開始更新,這些函數可以使用emit方法輸出新狀態,而每個狀態更改都會調用onChange方法 (其中包含當前狀態和下一個狀態)

實作:

設計一個計數器,初始值為0,並有加法方法,能對值做修改

我們在Android Studio 新建一個全新的專案,記得在pubspec.yaml添加bloc的依賴

建立 CounterCubit 類別,用來管理計數器的 int 狀態:

import 'package:bloc/bloc.dart';

class CounterCubit extends Cubit<int> { // Cubit<int>,管理的狀態為 int 
  //初始值設為0
  CounterCubit() : super(0);

  // 設計一個加法,當此方法被呼叫時,現在的狀態 (int) 就要加一,並透過emit方法更新為新的狀態
  void increase() => emit(state + 1);
}

我們來用用看

main.dart

import 'CounterCubit.dart';

void main() {
  //建立一個計數器物件
  CounterCubit cubit = CounterCubit();

  // 印出現在計數器的狀態 (CounterCubit 的狀態為 int)
  print(cubit.state); // 印出 0

  // 呼叫計數器的加法,該方法會去將狀態改變 (state + 1)
  cubit.increase();

  print(cubit.state); // 印出 1

  // 使用完把計數器 Cubit 物件關閉
  cubit.close();
}

onChange:可以被覆寫( override ),來處理當Cubit狀態改變時要做的行為

onError:可以被覆寫( override ),來處理當Cubit拋出異常時要做的行為

import 'package:bloc/bloc.dart';

class CounterCubit extends Cubit<int> {
  CounterCubit() : super(0);

  void increase() {
    print("increase");
    emit(state + 1);
  }
  
  void makeError() {
    addError(new Exception("test"));
  }

  @override
  void onChange(Change<int> change) {
    print("onChange:$change");
    super.onChange(change);
  }

  @override
  void onError(Object error, StackTrace stackTrace) {
    print('onError:$error, $stackTrace');
    super.onError(error, stackTrace);
  }
}

main.dart

import 'CounterCubit.dart';

void main() {

  CounterCubit cubit = CounterCubit();

  print(cubit.state); // 印出 0

  cubit.increase(); 
  /* 印出 
  increase
  onChange:Change { currentState: 0, nextState: 1 }
	*/
  print(cubit.state); // 印出 1

  cubit.makeError(); // 印出 onError:Exception: test, null,並拋出異常
}

若想對每個Cubit發生狀態改變或拋出異常時,都要客製化行為的話,可以使用BlocObserver

建立一個MyBlocObserver

import 'package:bloc/bloc.dart';

class MyBlocObserver extends BlocObserver {
  @override
  void onChange(Cubit cubit, Change change) {
    print("MyBlocObserver onChange:$cubit, $change");
    super.onChange(cubit, change);
  }

  @override
  void onError(Cubit cubit, Object error, StackTrace stackTrace) {
    print('MyBlocObserver onError:$cubit, $error, $stackTrace');
    super.onError(cubit, error, stackTrace);
  }
}

main.dart

import 'package:bloc/bloc.dart';

import 'CounterCubit.dart';
import 'MyBlocObserver.dart';

void main() {
  // 設定我們的 observer 
  Bloc.observer = MyBlocObserver();

  CounterCubit cubit = CounterCubit();

  print(cubit.state); // 印出 0

  cubit.increase(); 
  /* 印出 
  increase
  onChange:Change { currentState: 0, nextState: 1 }
  MyBlocObserver onChange:Instance of 'CounterCubit', Change { currentState: 0, nextState: 1 }
	*/
  print(cubit.state); // 印出 1

  cubit.makeError(); 
  /* 印出 
  onError:Exception: test, null
  MyBlocObserver onError:Instance of 'CounterCubit', Exception: test, null
  並拋出異常
  */
}

今天我們先介紹到這,下一篇進一步介紹 Bloc 的觀念以及使用


上一篇
Day16 Firebase
下一篇
Day18 Flutter 的狀態管理 BLoC (二)
系列文
從零開始的Flutter世界30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言